﻿using System.Collections.Generic;
using System.Web.UI;
using BrandonHaynes.Membership.Factors.PromptControls;
using DotNetNuke.Entities.Host;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Users;
using DotNetNuke.Security.Membership;
using DotNetNuke.Services.Mail;

namespace BrandonHaynes.Membership.Factors
	{
	/// <summary>
	/// A class that sends (and requires) a one-time password sent to a user via SMTP
	/// </summary>
	public class SmtpFactor : IAuthenticationFactor
		{
		private IDictionary<string, string> Attributes { get; set; }

		public SmtpFactor(IDictionary<string, string> attributes)
			{ Attributes = attributes; }

		#region IAuthenticationFactor Members

		public string Name { get { return "SMTP"; } }

		public void Authenticate(UserInfo user, Credential credential)
			{
			var email = user.GetUserProperty(Attributes.GetValueOrDefault("profileProperty", "EMail"));

			// If no e-mail address is specified, the user will not be authenticated
	        if (!string.IsNullOrEmpty(email))
	            {
				// We (in a small kludge) persist our one-time password in the MembershipUser.Comments property.
				// This property is not used by the DotNetNuke system, but it is also not secured against access.
				//		Probably should strengthen this -- perhaps via encryption or persistence in the profile.
				var aspnetUser = System.Web.Security.Membership.GetUser(user.Username);
				var onetimePassword = aspnetUser.Comment;

				if (credential[Name] == onetimePassword && !string.IsNullOrEmpty(credential[Name]))
	                {
					// The user has supplied the one-time password at credential[Name].  Proceed to authenticate.
					// Also, clear the one-time password.
	                aspnetUser.Comment = string.Empty;
	                System.Web.Security.Membership.UpdateUser(aspnetUser);
	                }
	            else
	                {
					// No one-time password was submitted, or it does not match the one generated.
					// Accordingly, create a new one...
	                aspnetUser.Comment = System.Web.Security.Membership.GeneratePassword(5, 0);
	                System.Web.Security.Membership.UpdateUser(aspnetUser);

					// Send it...
					Mail.SendMail(SourceEmailAddress, email, string.Empty, aspnetUser.Comment, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty);

					// And mark the credential as incomplete.
					credential.IncompleteFactors.Add(this);
	                }
				
				// Proceed with additional authentication (however, the credential might be incomplete as above)
				credential.Status = UserLoginStatus.LOGIN_SUCCESS;
				}
			else
				// No email address is bad news.  The user can't ever authenticate.
				// I'd love to just go ahead and authenticate when no e-mail address exists, but this leads to
				// attacks that are preferrable to just avoid.
				credential.Status = UserLoginStatus.LOGIN_USERLOCKEDOUT;
			}

		/// <summary>
		/// Retrieves a control used to collect credential information (in this case a prompt for the one-time password)
		/// </summary>
		public Control PromptControl
			{ get { return new PasswordPrompt(Name, Attributes); } }

		#endregion

		/// <summary>
		/// Retrieves the e-mail address to use as the "from" address for the one-time password.  Returns either
		/// the portal's e-mail address or (if it doesn't exist for some reason) the host address.
		/// </summary>
		private static string SourceEmailAddress
			{
			get
				{
				return
					PortalController.GetCurrentPortalSettings().Email ??
					Host.HostEmail;
				}
			}
		}
	}
